import { describe, expect, it } from 'vitest';

import { parseSignature } from './parser.js';

describe('SignatureParser', () => {
  describe('basic parsing', () => {
    it('parses a simple signature without description', () => {
      const sig = parseSignature('userQuestion:string -> modelAnswer:number');

      expect(sig.desc).toBeUndefined();
      expect(sig.inputs).toHaveLength(1);
      expect(sig.outputs).toHaveLength(1);

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(input0).toEqual({
        name: 'userQuestion',
        type: { name: 'string', isArray: false },
        isOptional: undefined,
        desc: undefined,
      });

      expect(output0).toEqual({
        name: 'modelAnswer',
        type: { name: 'number', isArray: false },
        isOptional: false,
        isInternal: false,
        desc: undefined,
      });
    });

    it('parses a signature with description', () => {
      const sig = parseSignature(
        '"This is a test" userQuestion:string -> modelAnswer:number'
      );

      expect(sig.desc).toBe('This is a test');
      expect(sig.inputs).toHaveLength(1);
      expect(sig.outputs).toHaveLength(1);
    });
  });

  describe('field descriptions', () => {
    it('parses fields with descriptions', () => {
      const sig = parseSignature(
        'userQuestion:string "input description" -> modelAnswer:number "output description"'
      );

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(input0.desc).toBe('input description');
      expect(output0.desc).toBe('output description');
    });

    it('handles both single and double quoted descriptions', () => {
      const sig = parseSignature(
        'userQuestion:string "double quotes", userParam:number \'single quotes\' -> modelAnswer:string "result"'
      );

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const input1 = sig.inputs[1] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(input0.desc).toBe('double quotes');
      expect(input1.desc).toBe('single quotes');
      expect(output0.desc).toBe('result');
    });
  });

  describe('optional fields', () => {
    it('parses optional input fields', () => {
      const sig = parseSignature(
        'requiredField:string, optionalField?:number -> modelAnswer:string'
      );

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const input1 = sig.inputs[1] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };

      expect(input0.isOptional).toBe(undefined);
      expect(input1.isOptional).toBe(true);
    });

    it('parses optional output fields', () => {
      const sig = parseSignature(
        'userQuestion:string -> requiredField:string, optionalField?:number'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };
      const output1 = sig.outputs[1] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.isOptional).toBe(false);
      expect(output1.isOptional).toBe(true);
    });
  });

  describe('internal marker', () => {
    it('parses output field with internal marker', () => {
      const sig = parseSignature('userQuestion:string -> modelAnswer!:number');
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };
      expect(output0.isInternal).toBe(true);
    });

    it('parses output field with both optional and internal markers', () => {
      const sig = parseSignature('userQuestion:string -> modelAnswer?!:number');
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };
      expect(output0.isOptional).toBe(true);
      expect(output0.isInternal).toBe(true);
    });

    it('throws error for input field with internal marker', () => {
      expect(() =>
        parseSignature('userQuestion!:string -> modelAnswer:number')
      ).toThrow(/cannot use the internal marker/);
    });
  });

  describe('array types', () => {
    it('parses array types', () => {
      const sig = parseSignature(
        'userQuestions:string[] -> modelAnswers:number[]'
      );

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(input0.type?.isArray).toBe(true);
      expect(output0.type?.isArray).toBe(true);
    });

    it('handles mix of array and non-array types', () => {
      const sig = parseSignature(
        'userQuestion:string, userQuestions:number[] -> modelAnswers:boolean[]'
      );

      const input0 = sig.inputs[0] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const input1 = sig.inputs[1] as {
        name: string;
        type: { name: string; isArray: boolean };
        isOptional: boolean;
        desc?: string;
      };
      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(input0.type?.isArray).toBe(false);
      expect(input1.type?.isArray).toBe(true);
      expect(output0.type?.isArray).toBe(true);
    });
  });

  describe('class types', () => {
    it('parses class types with single class', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "option1, option2"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['option1', 'option2']);
      }
    });

    it('parses class types with multiple options', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "positive, negative, neutral"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['positive', 'negative', 'neutral']);
      }
    });

    it('handles array of options', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryTypes:class[] "option1, option2"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      expect(output0.type?.isArray).toBe(true);
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['option1', 'option2']);
      }
    });

    it('throws error for input field with class type', () => {
      expect(() =>
        parseSignature('categoryType:class "a,b" -> modelAnswer:string')
      ).toThrow(/cannot use the "class" type/);
    });

    it('throws error for missing class options', () => {
      expect(() =>
        parseSignature('userQuestion:string -> categoryType:class ""')
      ).toThrow(/Missing class options/);
    });

    it('parses class types with pipe separator', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "option1 | option2 | option3"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['option1', 'option2', 'option3']);
      }
    });

    it('parses class types with mixed separators', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "option1, option2 | option3"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['option1', 'option2', 'option3']);
      }
    });

    it('parses class options with mixed separators and spacing', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "valid, option,with,comma"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['valid', 'option', 'with', 'comma']);
      }
    });

    it('parses class options with pipe separators and mixed spacing', () => {
      const sig = parseSignature(
        'userQuestion:string -> categoryType:class "valid | option|with|pipe"'
      );

      const output0 = sig.outputs[0] as {
        name: string;
        type:
          | { name: string; isArray: boolean }
          | { name: 'class'; isArray: boolean; options: string[] };
        isOptional: boolean;
        isInternal: boolean;
        desc?: string;
      };

      expect(output0.type?.name).toBe('class');
      if (output0.type?.name === 'class') {
        const classType = output0.type as {
          name: 'class';
          isArray: boolean;
          options: string[];
        };
        expect(classType.options).toEqual(['valid', 'option', 'with', 'pipe']);
      }
    });
  });

  describe('duplicate fields', () => {
    it('throws error for duplicate input fields', () => {
      expect(() =>
        parseSignature(
          'userQuestion:string, userQuestion:number -> modelAnswer:string'
        )
      ).toThrow(/Duplicate input field name/);
    });

    it('throws error for duplicate output fields', () => {
      expect(() =>
        parseSignature(
          'userQuestion:string -> modelAnswer:string, modelAnswer:number'
        )
      ).toThrow(/Duplicate output field name/);
    });

    it('throws error for fields in both input and output', () => {
      expect(() =>
        parseSignature('userQuestion:string -> userQuestion:string')
      ).toThrow(/appears in both inputs and outputs/);
    });
  });

  describe('error cases', () => {
    it('throws on empty signature', () => {
      expect(() => parseSignature('')).toThrow('Empty signature provided');
    });

    it('throws on missing arrow', () => {
      expect(() =>
        parseSignature('userQuestion:string modelAnswer:string')
      ).toThrow('Expected "->"');
    });

    it('throws on missing output fields', () => {
      expect(() => parseSignature('userQuestion:string ->')).toThrow(
        'No output fields specified'
      );
    });

    it('throws on invalid type', () => {
      expect(() =>
        parseSignature('userQuestion:invalid -> modelAnswer:string')
      ).toThrow('Invalid type "invalid"');
    });

    it('throws on unterminated string', () => {
      expect(() =>
        parseSignature(
          'userQuestion:string "unterminated -> modelAnswer:string'
        )
      ).toThrow('Unterminated string');
    });

    it('throws on unexpected content after signature', () => {
      expect(() =>
        parseSignature(
          'userQuestion:string -> modelAnswer:string extra content'
        )
      ).toThrow('Unexpected content after signature');
    });

    it('throws on invalid field name characters', () => {
      expect(() =>
        parseSignature('invalid-name:string -> modelAnswer:string')
      ).toThrow('Expected "->"');
    });

    it('throws on field names starting with numbers', () => {
      expect(() =>
        parseSignature('1name:string -> modelAnswer:string')
      ).toThrow('cannot start with a number');
    });
  });

  describe('whitespace handling', () => {
    [
      'userQuestion:string -> modelAnswer:number',
      ' userQuestion:string -> modelAnswer:number',
      'userQuestion:string -> modelAnswer:number ',
      ' userQuestion:string  ->  modelAnswer:number ',
      '\tuserQuestion:string -> modelAnswer:number\n',
    ].forEach((sigStr) => {
      it(`handles various whitespace patterns for signature: "${sigStr}"`, () => {
        const sig = parseSignature(sigStr);
        expect(sig.inputs).toHaveLength(1);
        expect(sig.outputs).toHaveLength(1);
        expect(sig.inputs[0]?.name).toBe('userQuestion');
        expect(sig.outputs[0]?.name).toBe('modelAnswer');
      });
    });
  });
});
